/*
 * Copyright (c) 2021 com.youkeyi. All rights reserved.
 *
 *  @author liuxiawang
 *  @location Shenzhen.China
 *  @date 4/8/21 3:23 PM
 */

package com.youkeyi.ddy.cloud.common.interceptor;

import com.alibaba.fastjson.JSON;
import com.youkeyi.ddy.cloud.common.filter.BodyReaderHttpServletRequestWrapper;
import com.youkeyi.ddy.cloud.common.utils.RedisUtils;
import com.youkeyi.ddy.cloud.common.utils.SecurityUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.*;

/**
 * 短时重复请求拦截器
 *
 * @Author liuxiawang
 * @Date 2021-04-08 15:23
 * @Location shenzhen.china
 */
@Component
public class RepeatRequestInterceptor extends HandlerInterceptorAdapter {

    private static final Logger logger = LoggerFactory.getLogger(RepeatRequestInterceptor.class);

    /**
     * 缓存数据库
     */
    private static final Integer CACHE_DB = 8;
    /**
     * 缓存超时时间(2秒)
     */
    private static final Integer CACHE_EXPIRE = 10;
    /**
     * 会话Token名称
     */
    private static final String SESSION_TOKEN = "token";
    /**
     * 会话请求时间戳
     */
    private static final String SESSION_STAMP = "RequestStamp";

    private static final List<String> excludeUri = new ArrayList<>();
    static {
        excludeUri.add("/system/doc/upload");
        excludeUri.add("/system/user/update/avatar");
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1。接口请求时带上请求的时间戳
        // 2。用token 方法名 请求路径 参数body，用md5加密，得到key作为redis的缓存key
        // 3。redis 缓存存入 key与时间戳
        // 4。下次有接口进来，通过第2步骤生成的缓存key，去查redis有咩有这个缓存
        // 5。没有就将这个key放进缓存
        // 6。有的话就拿这个缓存里面的数据的时间戳，与本次请求带来的时间戳做比较，大于2秒内可通行，小于的话不通行并提示类似"请勿重复使用此功能"
        boolean isRepeatSubmitRequest = false;
        String token = request.getHeader(SESSION_TOKEN);
        String requestStamp = request.getHeader(SESSION_STAMP);
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        String uri = request.getRequestURI();
        String methodName = null;
        try {
            methodName = method.getName();
        } catch (Exception e1) {
            e1.printStackTrace();
        }

        // 存在token 存在请求类型 请求类型为post/patch
        if (StringUtils.isNotBlank(token)
                && StringUtils.isNotBlank(methodName)
                && isWatchedMethod(request)
                && !excludeUri.contains(uri)
        ) {
            String bodyStr = null;
            if (request instanceof BodyReaderHttpServletRequestWrapper) {
                try {
                    bodyStr = ((BodyReaderHttpServletRequestWrapper) request).getBodyStr();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            String md5 = SecurityUtils.md5(String.format("%s-%s-%s-%s", token, methodName, uri, bodyStr), "123!@#qweQWE");
            String cacheKey = createRepeatSubmitCheckKey(md5);
            String cacheValue = RedisUtils.getStringFromJedis(CACHE_DB, cacheKey);

            // logger.info(String.format("防重复提交：Token：%s，MethodName：%s，Uri：%s，BodyStr：%s，CacheKey：%s，CacheValue：%s", token, methodName, uri, JSON.toJSONString(bodyStr), cacheKey, cacheValue));
            if (StringUtils.isNotBlank(cacheValue)) {
                RepeatSubmitRequest bo = null;
                try {
                    bo = JSON.parseObject(cacheValue, RepeatSubmitRequest.class);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (null != bo) {
                    Long preTimestamp = bo.getRequestTimestamp();
                    Long currentTimestamp = convert(requestStamp);
                    if (null != preTimestamp && null != currentTimestamp) {
                        isRepeatSubmitRequest = Math.abs(preTimestamp - currentTimestamp) > 1000;
                    }
                }
            } else {
                RedisUtils.addStringToJedis(CACHE_DB, cacheKey, JSON.toJSONString(new RepeatSubmitRequest(md5, convert(requestStamp))), CACHE_EXPIRE);
                isRepeatSubmitRequest = true;
            }
        } else {
            isRepeatSubmitRequest = true;
        }
        if (!isRepeatSubmitRequest) {
            logger.info(String.format("收到来自%s(%d)的重复提交请求：%s 参数：%s", request.getRemoteAddr(), request.getRemotePort(), request.getRequestURI(), methodName));
            response.setStatus(663);
            OutputStream out = response.getOutputStream();
            Map<String, String> resultMap = new HashMap<>();
            resultMap.put("code", "663");
            resultMap.put("message", "请勿重复使用此功能");
            out.write(JSON.toJSONString(resultMap).getBytes());
            return false;
        }
        return super.preHandle(request, response, handler);
    }

    private String createRepeatSubmitCheckKey(String md5) {
        return String.format("REPEAT_SUBMIT_CHECK_%s", md5);
    }

    /**
     * 是否为观察请求方法
     */
    private boolean isWatchedMethod(HttpServletRequest request) {
        List<String> watchedMethod = Arrays.asList("POST", "PATCH");
        String method = request.getMethod();
        return StringUtils.isNotBlank(method)
                && watchedMethod.contains(method.toUpperCase());
    }

    private String getBodyString(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request.getInputStream();
            reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));

            char[] bodyCharBuffer = new char[1024];
            int len;
            while ((len = reader.read(bodyCharBuffer)) != -1) {
                sb.append(new String(bodyCharBuffer, 0, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }

    private Long convert(String value) {
        try {
            return StringUtils.isNotBlank(value) ? Long.valueOf(value) : null;
        } catch (NumberFormatException e) {
            e.printStackTrace();
            return null;
        }
    }

    public static final class RepeatSubmitRequest {
        private String md5;
        private Long requestTimestamp;

        public RepeatSubmitRequest() {
        }

        private RepeatSubmitRequest(String md5, Long requestTimestamp) {
            this.md5 = md5;
            this.requestTimestamp = requestTimestamp;
        }

        public String getMd5() {
            return md5;
        }
        public void setMd5(String md5) {
            this.md5 = md5;
        }
        public Long getRequestTimestamp() {
            return requestTimestamp;
        }
        public void setRequestTimestamp(Long requestTimestamp) {
            this.requestTimestamp = requestTimestamp;
        }
    }
}
